package com.nebula.seckill.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.util.concurrent.TimeUnit;

/**
 * @Author: llq
 * @Date: 2019/9/24
 */
@Slf4j
public class RedisLock {

    private RedisTemplate<String,Object> redisTemplate;

    private static final int DEFAULT_ACQUIRY_RESOLUTION_MILLIS = 100;

    /**
     * Lock key path.
     */
    private String lockKey;

    /**
     * 锁超时时间，防止线程在入锁以后，线程宕机了，导致其他线程无限的执行等待
     */
    private int expireMsecs = 60 * 1000;

    /**
     * 锁等待时间，防止线程饥饿
     */
    private int timeoutMsecs = 10 * 1000;

    /**
     * 是用来判断是否释放锁
     */
    private volatile boolean locked = false;

    /**
     * Detailed constructor with default acquire timeout 10000 msecs and lock
     * expiration of 60000 msecs.
     *
     * @param lockKey
     *            lock key (ex. account:1, ...)
     */
    public RedisLock(RedisTemplate<String,Object> redisTemplate, String lockKey) {
        this.redisTemplate = redisTemplate;
        this.lockKey = lockKey + "_lock";
    }

    /**
     * Detailed constructor with default lock expiration of 60000 msecs.
     *
     */
    public RedisLock(RedisTemplate<String,Object> redisTemplate, String lockKey, int timeoutMsecs) {
        this(redisTemplate, lockKey);
        this.timeoutMsecs = timeoutMsecs;
    }

    /**
     * Detailed constructor.
     *
     */
    public RedisLock(RedisTemplate<String,Object> redisTemplate, String lockKey, int timeoutMsecs, int expireMsecs) {
        this(redisTemplate, lockKey, timeoutMsecs);
        this.expireMsecs = expireMsecs;
    }

    /**
     * @return lock key
     */
    public String getLockKey() {
        return lockKey;
    }

    /**
     * 就是根据key获取value
     * @param key
     * @return
     */
    public String get(final String key) {
        Object obj = null;
        try {
            obj = redisTemplate.execute(new RedisCallback<Object>() {
                @Override
                public Object doInRedis(RedisConnection connection) throws DataAccessException {
                    StringRedisSerializer serializer = new StringRedisSerializer();
                    byte[] data = connection.get(serializer.serialize(key));
                    connection.close();
                    if (data == null) {
                        return null;
                    }
                    return serializer.deserialize(data);
                }
            });
        } catch (Exception e) {
            log.error("get redis error, key : {}", key);
        }
        return obj != null ? obj.toString() : null;
    }


    public String set(final String key,final String value) {
        Object obj = null;
        try {
            obj = redisTemplate.execute(new RedisCallback<Object>() {
                @Override
                public Object doInRedis(RedisConnection connection) throws DataAccessException {
                    StringRedisSerializer serializer = new StringRedisSerializer();
                    connection.set(serializer.serialize(key), serializer.serialize(value));
                    return serializer;
                }
            });
        } catch (Exception e) {
            log.error("get redis error, key : {}", key);
        }
        return obj != null ? obj.toString() : null;
    }

    public boolean setNX(final String key, final String value) {
        Object obj = null;
        try {
            obj = redisTemplate.execute(new RedisCallback<Object>() {
                @Override
                public Object doInRedis(RedisConnection connection) throws DataAccessException {
                    StringRedisSerializer serializer = new StringRedisSerializer();
                    //仅仅key不存在时，则设置key的值为value,返回true,否则返回value
                    Boolean success = connection.setNX(serializer.serialize(key), serializer.serialize(value));
                    connection.close();
                    return success;
                }
            });
        } catch (Exception e) {
            log.error("setNX redis error, key : {}", key);
        }
        return obj != null ? (Boolean) obj : false;
    }


    /**
     * 设置redis中key的value，并且返回原来key对应的旧value
     * @param key
     * @param value
     * @return
     */
    private String getSet(final String key, final String value) {
        Object obj = null;
        try {
            //execute() 需要 RedisConnection 对象，通过 RedisConnection 操作 Redis 被称为低级抽象（Low-Level Abstractions）

            //opsFor 之类的被称为高级抽象（High-Level Abstractions），是为了提供更友好的模板类，底层还是调用的 execute()，
            // 需要 RedisConnection 对象。所以我觉得 opsFor 就是对 execute() 的进一步封装。
            obj = redisTemplate.execute(new RedisCallback<Object>() {
                @Override
                public Object doInRedis(RedisConnection connection) throws DataAccessException {
                    StringRedisSerializer serializer = new StringRedisSerializer();
                    //设置key的值，并返回key的旧值。
                    byte[] ret = connection.getSet(serializer.serialize(key), serializer.serialize(value));
                    connection.close();
                    return serializer.deserialize(ret);
                }
            });
        } catch (Exception e) {
            log.error("setNX redis error, key : {}", key);
        }
        return obj != null ? (String) obj : null;
    }


    /**
     *
     * 判断锁是否已经被调用
     * 获得 lock. 实现思路: 主要是使用了redis 的setnx命令,缓存了锁. reids缓存的key是锁的key,所有的共享,
     * value是锁的到期时间(注意:这里把过期时间放在value了,没有时间上设置其超时时间) 执行过程:
     * 1.通过setnx尝试设置某个key的值,成功(当前没有这个锁)则返回,成功获得锁
     * 2.锁已经存在则获取锁的到期时间,和当前时间比较,超时的话,则设置新的值
     *
     *
     * 先拿setnx来争抢锁，抢到之后，再用expire给锁加一个过期时间防止锁忘记了释放
     *
     * 如果在setnx之后执行expire之前进程意外crash或者要重启维护了，那会怎么样？
     * 问题 : 这个锁就永远得不到释放了
     * 解决 :set指令有非常复杂的参数，这个应该是可以同时把setnx和expire合成一条指令来用
     * @return true if lock is acquired, false acquire timeouted
     * @throws InterruptedException
     *             in case of thread interruption
     */
    public synchronized boolean lock() throws InterruptedException {
        int timeout = timeoutMsecs;
        while (timeout >= 0) {
            //当前时间+锁过期时间 = 线程拥有锁的实际到期时间
            long expires = System.currentTimeMillis() + expireMsecs + 1;
            // 锁到期时间
            String expiresStr = String.valueOf(expires);

            //redis里面key不存在，就来抢锁，只有一个能抢到锁，因为setNX是同步的 ****************
            //redis里面key不存在的情况 ？
            // 1 第一个线程
            // 2 上一个线程结算结束
            // 3 上一个线程redis自动过期
//            if (this.setNX(lockKey, expiresStr)) {   //当且仅当key不存在时,才存入key,value锁到期时间
//                // lock acquired
//                locked = true;
//                return true;
//            }
            // 这里不使用execute操作nx来自动判断是否存在key
            // 当且仅当redis里面的key（唯一）不存在时,才存入key,value,并且设置到期时间 *********
            // redisTemplate.hasKey(key) 判断是否有key对应的值，有 true,没有false
            if (!redisTemplate.hasKey(lockKey)){
                redisTemplate.opsForValue().set(lockKey,expiresStr , 30, TimeUnit.SECONDS);

                locked = true;
                return true;
            }

            //当锁还在redis中时，可能锁已经过期，没有线程在使用
            // 拿到当前redis里的到期时间
            // 如果是刚拿到锁，才加入的时间，是不可能过期的 当前时间+expireMsecs+1>当前时间
            // 如果是锁正在被别的线程使用，是可能过期的  旧时间+expireMsecs + 1  可能<  当前时间
            String currentValueStr = this.get(lockKey);
            //  锁被别的线程使用时，判断是否过期，过期了就执行
            //    锁还在redis中,所以currentValueStr不等于null , 这时判断锁是否过期
            if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
                // 判断是否为空，不为空的情况下，如果被其他线程设置了值，则第二个条件判断是过不去的
                // lock is expired  设置到期时间

                // 对key设置新值expiresstr,并返回原来的旧value值
                // 获取上一个锁到期时间，并设置现在的锁到期时间，此时锁被另一个线程调用
                String oldValueStr = this.getSet(lockKey, expiresStr);
                // 防止误删（覆盖，因为key是相同的）了他人的锁——这里达不到效果，这里值会被覆盖，但是因为什么相差了很少的时间，所以可以接受
                // 只有一个线程才能获取上一个线上的设置时间，因为jedis.getSet是同步的       *******************
                if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
                    //为什么这里获取了两次key的值来做比较 ？
                    // a b c d 4个线程，a线程之前拿到了锁过期了，b c d同时来获取锁，过期时间是一样的
                    // b 获取了a的旧value，c获取了a的旧value，d获取了a的旧value
                    // 但是只能有一个存进去redis（单线程同步）,获取旧的a的value。列如b存进去了
                    // 剩下来的c,d存进去的获取的是上一个b的value,而不是a的value，又因为b,c,d是同时来抢锁的，所以value里的时间是一样的，与a的value不同
                    // 所以只有b抢到了锁，来执行下单支付，剩下的要等b结束才能继续抢


                    // [分布式的情况下]:如过这个时候，多个线程恰好都到了这里，但是只有一个线程的设置值和当前值相同，他才有权利获取锁
                    // lock acquired
                    locked = true;
                    // 当前拿到锁的线程已过期，锁已释放，未被调用
                    return true;
                }
            }
            timeout -= DEFAULT_ACQUIRY_RESOLUTION_MILLIS;

            /*
             * 延迟100 毫秒, 这里使用随机时间可能会好一点,可以防止饥饿进程的出现,即,当同时到达多个进程,
             * 只会有一个进程获得锁,其他的都用同样的频率进行尝试,后面有来了一些进行,也以同样的频率申请锁,这将可能导致前面来的锁得不到满足.
             * 使用随机的等待时间可以一定程度上保证公平性
             * hread.sleep（10）让线程睡眠10秒（在未来10秒内不参与竞争），让系统重新计算其他所有的进程的优先级，
             * 降低自己的优先级，隐式的提高了其他进程的优先级，有效防止饥饿进程的出现
             * 系统进程优先级（优先级+饥饿时间统一计算的）
             */
            Thread.sleep(DEFAULT_ACQUIRY_RESOLUTION_MILLIS);

        }
        return false;
    }

    /**
     * 释放锁
     * Acqurired lock release.
     */
    public synchronized void unlock() {

        //判断锁是否已经超时
        //当前时间<过期时间证明还没有过期
        //如果已经超时了,锁已经被别人获取了,就没有必要释放锁了 **********
        //这里没有使用lua保证释放锁的原子性
        String lockvalue = this.get(lockKey);
        if (Long.parseLong(lockvalue) > System.currentTimeMillis()){
            if (locked) {
                redisTemplate.delete(lockKey);
                locked = false;
            }
        }

//        else(如果超时，锁已经被别的线程获取了，就没有必要释放锁了)
        //存到redis里面有一个过期时间 a
        //value里面有一个过期时间 b
        // a >= b ,因为不能b的时间内还没结算，就已经过期了，让别的线程来抢到锁


//        String checkAndDelScript = "
//        //redisTemplate.execute来调用luaif redis.call('get', KEYS[1]) > ARGV[1] then " +
////                "return redis.call('del', KEYS[1]) " +
////                "else " +
////                "return 0 " +
////                "end";
//        redisTemplate.execute(checkAndDelScript,1,lockKey,)
//        jedis.eval(checkAndDelScript, 1, lockKey, lockValue);
    }
}
